#!/usr/bin/perl -w
use strict;
use warnings;
use Getopt::Std;
use XML::LibXML;

my $version = "0.1-beta";
my %typeSize = 
    (
        'bool'      => 1,
        'string'    => 2,
        'byte'      => 1,
        'ubyte'     => 1,
        'short'     => 2,
        'int'       => 4,
        'long'      => 8,
        'float'     => 4,
        'double'    => 8,
        'array'     => 0
        
    );
my %typeReplace = 
    (
        'bool'      => 'bool',
        'string'    => 'std::string',
        'byte'      => 'int8_t',
        'ubyte'     => 'uint8_t',
        'short'     => 'int16_t',
        'int'       => 'int32_t',
        'long'      => 'int64_t',
        'float'     => 'float',
        'double'    => 'double',
        'array'     => 'std::vector'
    );
my %typeDefault = 
    (
        'bool'      => 'false',
        'string'    => '""',
        'byte'      => '0',
        'ubyte'     => '0',
        'short'     => '0',
        'int'       => '0',
        'long'      => '0',
        'float'     => '.0',
        'double'    => '.0',
        'array'     => ''
    );
my %typeWriteFunc = 
    (
        'bool'      => 'jsw.writeBoolean',
        'string'    => 'jsw.writeUTF',
        'byte'      => 'jsw.writeByte',
        'ubyte'     => 'jsw.writeByte',
        'short'     => 'jsw.writeShort',
        'int'       => 'jsw.writeInt',
        'long'      => 'jsw.writeLong',
        'float'     => 'jsw.writeFloat',
        'double'    => 'jsw.writeDouble'
    );
my %typeCast = 
    (
        'bool'      => 'JavaStreamWrapper::readBoolean',
        'string'    => 'JavaStreamWrapper::readUTF',
        'byte'      => 'JavaStreamWrapper::readByte',
        'ubyte'     => 'JavaStreamWrapper::readUnsignedByte',
        'short'     => 'JavaStreamWrapper::readShort',
        'int'       => 'JavaStreamWrapper::readInt',
        'long'      => 'JavaStreamWrapper::readLong',
        'float'     => 'JavaStreamWrapper::readFloat',
        'double'    => 'JavaStreamWrapper::readDouble'
    );
my %opts = ();

# Paramètres
getopts("i:s:h:v", \%opts);

if ($opts{v})
{
    print "build_packets.pl v$version\n";
    exit 0;
}

# Fichier source renseigné ?
if (!defined($opts{i}))
{
    print "Le fichier source n'a pas été spécifié (option -i)";
    exit 1;
}

if (!defined($opts{s}))
{
    print "Le dossier de destination des sources n'a pas été spécifié (option -s)";
    exit 1;
}
my $srcDir = $opts{s};


if (!defined($opts{h}))
{
    print "Le dossier de destination des entêtes n'a pas été spécifié (option -h)";
    exit 1;
}
my $headerDir = $opts{h};

# Fichier source valide ?
if (! -e $opts{i})
{
    print "Le fichier source `$opts{i}' n'existe pas";
    exit 1;
}

# Ouverture du fichier
my $parser = XML::LibXML->new();
$parser->validation(1);

my $tree;
eval { $tree = $parser->parse_file($opts{i}) };
    die "Le fichier $opts{i} est mal formé (DTD) :\n$@" if $@;

# Racine du document XML
my $root = $tree->getDocumentElement;

# Validation XSD du fichier XML
my $xsdFile = $root->getAttribute('xsi:noNamespaceSchemaLocation');
my $xsdSchema = XML::LibXML::Schema->new( location => $xsdFile );

eval { $xsdSchema->validate($tree) };
    die "Le fichier $opts{i} est mal formé (XSD) :\n$@" if $@;
    
# Traitement des paquets
my @packets = $root->getElementsByTagName('packet');

print "Traitement des paquets ... " . @packets . " paquets décrits :\n";
my $packetHeaders = '';
my $pfsInit = '';

foreach my $packet (@packets) 
{
    my $id = $packet->getAttribute('id');
    my $name = $packet->getAttribute('name');
    my @pts = $packet->getElementsByTagName('pts');
    my @pfs = $packet->getElementsByTagName('pfs');
    my $className = '';
    
    print "  > $id - $name\n";
    
    if (scalar @pts)
    {
        print "     [-->] ";
        handlePacket('pts', $id, $name, @pts);
        $packetHeaders .= '#include <packet/' 
            . className('pts', $id, $name) . ".h>\n";
    }
    
    if (scalar @pfs)
    {
        print "     [<--] ";
        handlePacket('pfs', $id, $name, @pfs);
        $packetHeaders .= '#include <packet/' 
            . className('pfs', $id, $name) . ".h>\n";
        $pfsInit .= "        case 0x$id:\n"
            . '            *packet = static_cast<McPacketFromServer*>(new ' 
            . className('pfs', $id, $name) . "());\n"
            . "            return 0;\n\n";
    }
}

print "   Creating header file $headerDir/packets.h ...";
open( my $hFile, '>',  "$headerDir/packets.h")
  or die("error.\n");

print {$hFile} "/* packets.h
 *
 * --
 * Copyright (c) 2012, Christophe Huriaux
 * Distributed under the terms of the MIT license.
 * http://www.opensource.org/licenses/mit-license.php
 * --
 */\n\n";
print {$hFile} "#ifndef MC_PACKETS_H\n";
print {$hFile} "#define MC_PACKETS_H\n\n";
print {$hFile} "$packetHeaders\n\n";
print {$hFile} "int packetInitialize(McPacketFromServer** packet, unsigned char id);\n";
print {$hFile} "#endif // MC_PACKETS_H\n";
close($hFile);
print "done.\n";

print "   Creating source file $srcDir/packets.cpp ...";
open( my $cFile, '>',  "$srcDir/packets.cpp")
  or die("error.\n");
print {$cFile} "/* packets.cpp
 *
 * --
 * Copyright (c) 2012, Christophe Huriaux
 * Distributed under the terms of the MIT license.
 * http://www.opensource.org/licenses/mit-license.php
 * --
 */\n\n";
print {$cFile} "#include <packet/packets.h>\n\n";
print {$cFile} "int packetInitialize(McPacketFromServer** packet, unsigned char id)\n{\n";
print {$cFile} "    switch (id)\n    {\n";
print {$cFile} $pfsInit;
print {$cFile} "        default:\n";
print {$cFile} "            *packet = NULL;\n";
print {$cFile} "            return -1;\n    }\n}\n";
close($cFile);
print "done.\n";

#
# packetSize(fields, humanReadable)
#
#   Compute the size of the packet in bytes
#

sub packetSize
{
    my @fields = @_;
    my $humanReadable = pop(@fields);
    my $lenString = '1 + ';
    my $lenAdditional = '';
    
    foreach my $field (@fields)
    {
        my $type = $field->getAttribute('type');
        my $name = $field->getAttribute('name');
        my $unused = $field->getAttribute('unused');
    
        $lenString .= $typeSize{$type} . ' + ';
        $lenAdditional .= "$name.length() + " 
            if ($type eq 'string' && $unused eq 'false');
    }
    
    substr($lenString, -3) = '';
    substr($lenAdditional, -3) = '';
    
    if ($humanReadable == 1) 
    {
        eval('$lenString = ' . $lenString);
        $lenAdditional = 'strings length' if (length($lenAdditional));
    }
    
    if (length($lenAdditional))
    {
        return $lenString . ' + ' . $lenAdditional;
    } else
    {
        return $lenString;
    }
}



#
# handlePacket(type, id, name, descriptor)
#
#   Parse the packet data and create the files
#

sub handlePacket 
{
    my ($type, $id, $name, $desc) = @_;
    my @fields = $desc->getElementsByTagName('field');
    
    print @fields . " champs\n";
    
    my $className = className($type, $id, $name);
    my $constructorDecl = '';
    my $members = '';
    my $settersDecl = '';
    my $gettersDecl = '';
    my $setters = '';
    my $getters = '';
    my $ctorInitDefault;
    my $ctorInit;
    my $forge = '';
    my $setField = "        case 0:\n        default: ";
    my $setFieldCounter = 1;
    my $setFieldStatics = '';
    
    
    $ctorInitDefault =  "McPacketToServer(0x$id), " if ($type eq 'pts');
    $ctorInitDefault =  "McPacketFromServer(0x$id), " if ($type eq 'pfs');
    $ctorInit =  "McPacketToServer(0x$id), " if ($type eq 'pts');
    $ctorInit =  "McPacketFromServer(0x$id), " if ($type eq 'pfs');

    foreach my $field (@fields)
    {
        my $fType = $field->getAttribute('type');
        my $name = $field->getAttribute('name');
        my $unused = $field->getAttribute('unused');
        my $comment = $field->getAttribute('comment');
        my $array = $field->getAttribute('array');
        my $arraytype = $field->getAttribute('arraytype');
        my $arraysize = $field->getAttribute('arraysize');
        
        my $listField = ($unused eq 'false' && $fType ne 'array');
        my $putFieldInMembers = ($unused eq 'false' || $type eq 'pfs');
        
        print "       ($fType) ";
        print "# " if ($unused eq "true");
        print "$name\n";
        
        if ($listField)
        {
            # Constructor prototype
            $constructorDecl .= $typeReplace{$fType} . ' ' . $name . ', ' ;
            
            # Setters
            $settersDecl .=  '    void set' . ucfirst($name) . '(' . 
                $typeReplace{$fType} . ' ' . $name . ");\n";
            $setters .= "void $className" . '::set' . ucfirst($name) . '(' . 
                $typeReplace{$fType} . ' ' . $name . ")\n{\n" .
                "    this->$name = $name;\n}\n\n";
                
            # Constructor initializers
            $ctorInit .= "$name(". $name .'), ';
        }
        
        if ($putFieldInMembers)
        {   
            if ($fType ne 'array')
            {
                # Class members
                $members .=  '    ' . $typeReplace{$fType} 
                    . ' ' . $name . ";\n";
            
                # Getters
                $gettersDecl .=  '    ' . $typeReplace{$fType} . ' get' 
                    . ucfirst($name) . "();\n";
                $getters .= $typeReplace{$fType} . " $className" 
                    . "::get" . ucfirst($name) ."()\n{\n    return $name;\n}\n\n";
            } else
            {
                $members .=  '    ' . $typeReplace{$fType} 
                    . '<' . $typeReplace{$arraytype} . '> ' . $name . ";\n"; 
                    
                # Getters
                $gettersDecl .=  '    ' . $typeReplace{$fType} 
                    . '<' . $typeReplace{$arraytype} . '>& get' 
                    . ucfirst($name) . "();\n";
                $getters .= $typeReplace{$fType} 
                    . '<' . $typeReplace{$arraytype} . ">&  $className" 
                    . "::get" . ucfirst($name) ."()\n{\n    return $name;\n}\n\n";
            }
            
            # Default constructor initialiers
            $ctorInitDefault .= "$name(". $typeDefault{$fType} .'), ';
            
            # setField()
            if ($fType ne 'string' && $fType ne 'array')
            {
                $setField .= 'return ' . $typeSize{$fType} . ";\n        "
                    . "case $setFieldCounter: $name = " 
                    . $typeCast{$fType} . '(value); ';
                $setFieldCounter++;
            }
            elsif ($fType eq 'string')
            {
                $setFieldStatics = "    static int16_t slen;\n" 
                    if (length($setFieldStatics) == 0);
                    
                $setField .= "return 2;\n        "
                    . "case $setFieldCounter: slen = " 
                    . $typeCast{'short'} . "(value); return 2*slen;\n";
                $setFieldCounter++;
                $setField .= "        case $setFieldCounter: $name = " 
                    . $typeCast{$fType} . '(value, slen); ';
                $setFieldCounter++;
            }
            elsif ($fType eq 'array')
            {
                $setField .= 'return ' . $arraysize . ";\n        "
                    . "case $setFieldCounter: $name.reserve($arraysize); JavaArrayWrapper<"
                    . $typeReplace{$arraytype} . '>::readArray'
                    . "(value, $name, $arraysize); ";
                $setFieldCounter++;
            }
        }
       
        # forge()
        if ($type eq 'pts') {
            if ($unused eq 'true' && $fType eq 'string')
            {
                $forge .= '    ' . $typeWriteFunc{'short'} . "(0);\n";
            } else
            {
                $forge .= '    ' . $typeWriteFunc{$fType} . '(';
                $forge .= $name if ($unused eq 'false');
                $forge .= $typeDefault{$fType} if ($unused eq 'true');
                $forge .= ");\n";
            }
        }
    }
    substr($constructorDecl, -2) = '';
    substr($ctorInitDefault, -2) = '';
    substr($ctorInit, -2) = '';
    
    
    #
    # Header
    #
    
    print "       Creating header file $headerDir/$className.h ...";
    open( my $headerFile, '>',  "$headerDir/$className.h")
      or die("error.\n");
      
    print {$headerFile} "/* $className.h
 *
 * --
 * Copyright (c) 2012, Christophe Huriaux
 * Distributed under the terms of the MIT license.
 * http://www.opensource.org/licenses/mit-license.php
 * --
 */\n\n";
    print {$headerFile} '#ifndef MCPACKET_' . $type . $id . "_H\n";
    print {$headerFile} '#define MCPACKET_' . $type . $id . "_H\n\n";
    
    print {$headerFile} "#include <string>\n";
    print {$headerFile} "#include <iostream>\n";
    print {$headerFile} "#include <stdint.h>\n";
    print {$headerFile} "#include <vector>\n";
    print {$headerFile} "#include <packet/McPacket.h>\n\n";
    
    print {$headerFile} "class $className : public ";
    print {$headerFile} 'McPacketToServer' if ($type eq 'pts');
    print {$headerFile} 'McPacketFromServer' if ($type eq 'pfs');
    print {$headerFile} "\n{\n";
    print {$headerFile} $members . "\n";
    print {$headerFile} "public:\n";
    print {$headerFile} "    $className($constructorDecl);\n"
        if ($type eq 'pts' && length($constructorDecl));
    print {$headerFile} "    $className();\n\n";
    
    print {$headerFile} "    void forge(std::ostream& out);\n" 
        if ($type eq 'pts');
    print {$headerFile} "    int16_t setField(uint8_t field, void* value);\n"
        if ($type eq 'pfs');
    print {$headerFile} "    unsigned int length();\n\n";
    
    print {$headerFile} $gettersDecl . "\n";
    print {$headerFile} $settersDecl if ($type eq 'pts');
    print {$headerFile} "};\n\n";
    
    print {$headerFile} '#endif // MCPACKET_' .  $type . $id . "_H\n";
    close($headerFile);
    
    print "ok.\n";
  
    print "       Creating c++ file $srcDir/$className.cpp ...";
    open( my $cppFile, '>',  "$srcDir/$className.cpp")
      or die("error.\n");
    print {$cppFile} "/* $className.cpp
 *
 * --
 * Copyright (c) 2012, Christophe Huriaux
 * Distributed under the terms of the MIT license.
 * http://www.opensource.org/licenses/mit-license.php
 * --
 */\n\n";
 
    print {$cppFile} "#include <packet/$className.h>\n";
    print {$cppFile} "#include <wrapper/JavaStreamWrapper.h>\n\n";
    print {$cppFile} $className . '::' . "$className()\n";
    print {$cppFile} "    : $ctorInitDefault\n{\n}\n\n";
    
    if ($type eq 'pts' && length($constructorDecl))
    {
        print {$cppFile} $className . '::' 
            . "$className($constructorDecl)\n";
        print {$cppFile} "    : $ctorInit\n{\n}\n\n";
    }
    
    print {$cppFile} 'unsigned int ' . $className . "::length()\n{\n";
    print {$cppFile} '    return ' . packetSize(@fields, 0) . ";\n}\n\n";
        
    if ($type eq 'pts')
    {
        print {$cppFile} 'void ' . $className . "::forge(std::ostream& out)\n{\n";
        print {$cppFile} "    JavaStreamWrapper jsw(out);\n\n";
        print {$cppFile} "    jsw.writeByte(getPacketId());\n$forge";
        print {$cppFile} "\n}\n\n";
    }
    
    if ($type eq 'pfs')
    {
        print {$cppFile} 'int16_t ' . $className 
            . "::setField(uint8_t field, void* value)\n{\n";
        print {$cppFile} "$setFieldStatics\n";
        print {$cppFile} "    switch (field)\n    {\n";
        print {$cppFile} $setField . 'return -1;';
        print {$cppFile} "\n    }\n}\n\n";
    }
    
    print {$cppFile} $getters . "\n";
    print {$cppFile} $setters . "\n"  if ($type eq 'pts');
    close($cppFile);
    
    print "ok.\n";
      
    print "\n";
}


#
#  Compute the class name for a given packet
#
sub className {
    my ($type, $id, $name) = @_;
    
    return $type . $id . $name;
}
